昨天複習了工廠函式,今天來連結到Vue滿常見的設計模式-組合式邏輯(Composable)
。
Composable
是一個相對通俗的概念,通常指的是可以在多個元件之間重複使用的邏輯模塊
,並且結合了 Vue 3 的組合式 API(Composition API)來實現。
Composable
通常利用 Vue 的響應式系統和生命週期鉤子
,讓程式碼更加模組化、彈性化和易於維護。這不僅提高了邏輯的可重用性,也有效避免了元件檔案因為業務邏輯過於複雜而變得過於龐大,有助於保持程式碼的清晰和結構化。
Composable
和一般JavaScript通用函式
差別Composable
基本用法和案Composable
設計步驟和注意事項是 Vue 3 新的特性,主要用在多個组件之間的重複性共享邏輯,透過 Vue 提供的 API 來封裝如狀態管理、副作用(watch)、計算屬性(compute)
等,
像是能夠存取和管理 Vue 的響應式系統
、可以包含例如 API 請求、訂閱等,也可以和 Vue 的生命週期鉤子(如 onMounted、onUnmounted 等)結合使用
。
Utility
通常是指在程式碼中執行特定任務或轉換資料的純函數,它們與 Vue 所提供的API無關
。
通常是與框架無關的通用函數,獨立於Vue 的響應式系統和生命周期鉤子,可以在任何 JavaScript 執行環境中使用
,不限於 Vue.js (不帶有Vue 提供的API)。
通常用於資料轉換、格式化、簡單計算
等,因為是純函数,意味著给定相同的輸入值,總是返回没有副作用的輸出,設計上有利於單元測試。
// 將日期格式化為 YYYY-MM-DD
export function formatDate(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = ('0' + (d.getMonth() + 1)).slice(-2);
const day = ('0' + d.getDate()).slice(-2);
return `${year}-${month}-${day}`;
}
總結歸納來說,如果函式中使用了 Vue 才有的功能 (例如 ref
或是 onMounted
),我們就會稱它為組合式函式而不是普通的函式。
我們可以簡單創建一個 useCounter 函式
,用來管理計數器邏輯。函式返回值包括計數器的響應式資料狀態
和一些更新資料邏輯的函式
,比如增加、減少和重置計數等功能函式。
// composables/useCounter.js
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = initialValue;
};
return {
count,
increment,
decrement,
reset,
};
}
// 元件中引用使用
<template>
<div>
<p>每一個組件的計數:{{ count }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter';
</script>
其實跟昨天複習工廠函式有提到,是 JavaScript 模組執行作用域的典型應用
。
像 useCounter
這類的 Composable
函數每次被呼叫時,都會透過回傳值建立一個新的閉包環境(closure)
,裡面封存著當下執行環境(execution context)下使用過的資料,從而產生獨立的狀態實例
。
當你在多個元件中呼叫同一個 Composable(如 useCounter),每個元件都會獲得一個獨立的計數器實例。這是因為每次呼叫 Composable 函數時,都會建立一個新的執行上下文和狀態(如 ref、reactive 等)
。
這些狀態被封閉在獨立執行環境(execution context)
,並且只有在對應的元件中使用。即使多個元件重複使用了同一個邏輯,它們之間的資料流仍然是獨立的,不會互相干擾。每個元件都有自己的狀態副本。
如果以昨天複習過的工廠函式的思維
,怎麼定義內部變數和決定要公開的方法或資料狀態應該滿類似的:
use
開頭公開和私有狀態、方法
)最好以ref定義響應式變數
多個 composable 引用進入同一元件時,注意命名衝突
瀏覽器事件偵聽器記得在元件卸載時移除
import { ref, watch, toValue } from 'vue'
// 當改變 url時 composable function會重新fetch api
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// Reset state before fetching...
data.value = null
error.value = null
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
// Use watch to explicitly monitor changes to url
watch(url, () => {
fetchData()
}, { immediate: true }) // Immediate option to trigger fetch on initial setup
return { data, error }
}
compsable是由回傳值去決定那些操作方法或內部資料你需要暴露給使用者使用
,當然如果覺得某些資料很重要暴露出去給畫面渲染,但又不希望被更動到,可以選擇用readonly鎖住。
通常實務上自己設計會將暴露的出去的資料另外設計update function
,使用者在更新資料時更加有意識地使用 update 函數,而不是變動原始資料。
import { ref, readonly } from 'vue';
export function useCounter() {
// 定義內部可變動的狀態
const count = ref(0);
// 使用 `readonly` 將 count 鎖住,避免外部直接修改
const readonlyCount = readonly(count);
// 更新函數,提供修改 count 的介面
const updateCount = (value) => {
// 可以做一些簡單型別檢查 也可以使用customRef 看資料結構複雜度
if (typeof value === 'number') {
count.value = value;
} else {
console.warn('Invalid value, count must be a number');
}
};
// 回傳 readonly 的 count 和更新函數
return {
count: readonlyCount, // 這裡暴露出去的是 readonly 版本的 count
updateCount // 這裡提供的是修改的途徑
};
}
composable返回值,建議是以 ref 物件單獨放在一個普通物件
裡返回,因為上次有提到reactive單獨解構會回傳純值,喪失響應式
。
為了避免解構 reactive 物件時失去響應性,通常會使用一個特殊的API toRefs
將 reactive 物件的屬性轉換為 ref,這樣即使解構後屬性仍然是響應式的,不過還是建議composable 函式還是盡量以 ref當作返回值為主
,開發複雜度比較低。
import { reactive, toRefs } fro
export function useExample() {
const state = reactive({
count: 0,
message: 'Hello',
});
// 如果直接解構reactive,若解構出來的是primitive type 只會返回該數值,不會再掛入proxy
const { count, message } = toRef(state);
return {
count,
message,
};
}
如果你在一個 composable 中使用了 watch 或 watchEffect
偵聽器,通常不需要手動卸載它們。 Vue 會自動管理這些偵聽器的生命週期,元件被卸載時(onMounted)與元件相關的偵聽器也會自動被清理
如果你在 composable 中使用了與元件資料流無關偵聽器(如 window 物件上的事件)
,你需要手動清理這些偵聽器。這種情況並不會自動管理,因為它們超出了 Vue 的生命週期管理範圍,是屬於非框架資料流外(uncontrolled data flow),類似上次說的非受控現象,記得同步隨元件消毀移除監聽器。
// 在組件掛載時,同步設定window監聽器
onMounted(() => {
window.addEventListener('resize', handleResize);
});
// 在組件卸載時,也必須同步卸載window監聽器,避免記憶體洩漏
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
這是我在面對實務開發上,分解商業需求常遇到的,我們製作composable
設計上往往想說,要盡量讓它的重複邏輯利用性
達到最高(變得更完美,能夠相容各種需求),不過實際開發上滿常遇到業務邊際需求
:
最近滿喜歡的一種思維,沒辦法共用,就別強迫自己綁在共用compoasble函式裡,即便他們有90%相似
~
很多開發人員在面對新功能時會選擇修改既有的組合式函式而不是製作一個新的,只為了讓他能夠在更多元件中被使用。在這種情況下,我們常常見到組合式函式「失控」 —為了能處理各種(邊緣)情況,越來越多的參數和方法被加入,導致事情遠比它應有的還要複雜;而且隨著時間推移,重構/替換疊加的成本只會越來越高。若您發現舊的組合式函式開始變得過於複雜,不要害怕建立新的組合式函式。
若某個元件的商業邏輯有點複雜,甚至很特殊,即使整個應用程式中只有一個元件在使用這個功能,將這個特殊(奇怪)的功能「切」(模組化) 成數個小功能 (組合式函式) 分離出來是完全沒問題的。
因為通常組合式函式
會放在全域composable資料夾
底下,在開發設計上往往會以相容給其他組件使用為主,不過某些頁面(page)層元件
,包裹的商業邏輯比較特殊,我們設計共用性時常遇到困難~
所以在某需特殊商業邏輯沒那麼共用情況下,可以選擇在pages資料夾底
下開發新的屬於該頁面的composables函式
。
/src
├── composables/
│ └── useGlobalFeature.js // 全局大家可以共用的
├── pages/
│ ├── PageA/
│ │ ├── PageA.vue
│ │ └── usePageALogic.js // 特殊商業邏輯
│ └── PageB/
│ ├── PageB.vue
│ └── usePageBLogic.js // 特殊商業邏輯
Composable
是 Vue 3 中的重要概念,旨在提供一種模組化、彈性化的方式來重用邏輯。相比傳統的 JavaScript 通用函式,組合式函式能夠整合 Vue 的響應式系統和生命週期鉤子,使開發者能夠更有效地處理元件間的狀態共享、非同步請求、和副作用處理
。
設計組合式函式時,應該考慮命名規則、參數設置、狀態封裝,以及暴露的操作方法
,也可以透過 readonly
保護重要的資料不被意外修改。
組合式函式的目標並不僅僅是考量重用邏輯,還需要考慮可維護性和彈性。遇到特定的邊緣需求
時,不應勉強將所有邏輯納入共用的組合式函式,而是可以根據業務場景拆分出多個功能模塊來處理特殊需求。